OpenGL 基本概念之 Shader 学习
本篇笔记的代码基于上篇笔记的 代码 进行拓展
基本语法就不再重复的写了,这里直接参考 GLSL 详解(基础篇)
这里就介绍一个重组的语法,这个重组可以把一些已有的变量重新组装成新的变量
vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
// Or
vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);
如果单独为 Shader 创建一个文件,一般是以下拓展名
- 片段着色器
.frag
- 顶点着色器
.vert
一个典型的着色器有下面的结构:
#version version_number // 声明版本
in type in_variable_name; // 输入和输出变量
in type in_variable_name;
out type out_variable_name;
uniform type uniform_name;
// 每个着色器的入口点都是main函数
int main()
{
// 处理输入并进行一些图形操作
...
// 输出处理过的结果到输出变量
out_variable_name = weird_stuff_we_processed;
}
顶点着色器的每个输入变量也叫顶点属性(Vertex Attribute),能声明的顶点属性是有上限的,它一般由硬件来决定。
OpenGL 确保至少有 16 个包含 4 分量的顶点属性可用,但是有些硬件或许允许更多的顶点属性,可以查询 GL_MAX_VERTEX_ATTRIBS 来获取具体的上限:
var nrAttributes int32
gl.GetIntegerv(gl.MAX_VERTEX_ATTRIBS, &nrAttributes)
log.Printf("Number of Vertices currently supported: %d", nrAttributes)
// 2022/01/03 11:34:01 Number of Vertices currently supported: 16
Shader 的输入与输出
每个着色器都有输入和输出,这样才能进行数据交流和传递。GLSL 定义了 in
和 out
关键字专门来实现这个目的。
顶点着色器它从顶点数据中直接接收输入,为了定义顶点数据该如何管理,使用 location 这一元数据指定输入变量,这样才可以在 CPU 上配置顶点属性。顶点着色器需要为它的输入提供一个额外的 layout 标识,这样才能把它链接到顶点数据。
// example
layout (location = 0) in vec3 aPos;
然后是片段着色器,它需要一个 vec4 颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。如果在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)。
例如:让顶点着色器为片段着色器决定颜色
顶点着色器
#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0
out vec4 vertexColor; // 为片段着色器指定一个颜色输出
void main()
{
gl_Position = vec4(aPos, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数
vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色
}
片段着色器
#version 330 core
out vec4 FragColor;
in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)
void main()
{
FragColor = vertexColor;
}
输出效果:
看看能否从应用程序中直接给片段着色器发送一个颜色!
Uniform 发送数据
Uniform 是一种从 CPU 中的应用向 GPU 中的着色器发送数据的方式,但 uniform 和顶点属性有些不同。
首先,uniform 是全局的(Global)。全局意味着 uniform 变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把 uniform 值设置成什么,uniform 会一直保存它们的数据,直到它们被重置或更新。
通过 uniform 设置三角形的颜色:
#version 330 core
out vec4 FragColor;
uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量
void main()
{
FragColor = ourColor;
}
因为 uniform 是全局变量,我们可以在任何着色器中定义它们,而无需通过顶点着色器作为中介。所以这里定义在片段着色器里面
如果声明了一个 uniform 却在 GLSL 代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点!
首先需要找到着色器中 uniform 属性的索引/位置值。当得到 uniform 的索引/位置值后,我们就可以更新它的值了。这次我们不去给像素传递单独一个颜色,而是让它随着时间改变颜色:
timeValue := glfw.GetTime()
greenValue := float32((math.Sin(timeValue) / 2.0) + 0.5)
// \x00 表示 ascii 码为 0 的字符,这里用来表示字符串的结尾
vertexColorLocation := gl.GetUniformLocation(program, gl.Str("ourColor" + "\x00"))
gl.UseProgram(program)
gl.Uniform4f(vertexColorLocation, 0.0, greenValue, 0.0, 1.0)
首先我们通过 glfw.GetTime()
获取运行的秒数。然后我们使用 sin 函数让颜色在 0.0 到 1.0 之间改变,最后将结果储存到 greenValue 里。
接着,我们用 gl.GetUniformLocation
查询 uniform ourColor 的位置值,如果返回 -1 就代表没有找到这个位置值。
最后,我们可以通过 Uniform4f 函数设置 uniform 值(因为这里是改变 vec4 的值所以使用 Uniform4f,如果是 vec3 就使用 Uniform3f,依次类推)。
注意,查询 uniform 地址不要求你之前使用过着色器程序,但是更新一个 uniform 之前你必须先使用程序,因为它是在当前激活的着色器程序中设置 uniform 的。
假如我们打算为每个顶点设置一个颜色的时候该怎么办?在这一问题上更好的解决方案是在顶点属性中包含更多的数据
为顶点添加更多属性
如下,为每个顶点添加颜色属性
var (
vertices = []float32{
// 位置 // 颜色
0.5, -0.5, 0.0, 1.0, 0.0, 0.0, // 右下
-0.5, -0.5, 0.0, 0.0, 1.0, 0.0, // 左下
0.0, 0.5, 0.0, 0.0, 0.0, 1.0, // 顶部
}
indices = []uint32{ // 注意索引从0开始!
0, 1, 2, // 第一个三角形
}
)
由于现在有更多的数据要发送到顶点着色器,我们有必要去调整一下顶点着色器,使它能够接收颜色值作为一个顶点属性输入。需要注意的是我们用 layout 标识符来把 aColor 属性的位置值设置为 1:
#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为 0
layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1
out vec3 ourColor; // 向片段着色器输出一个颜色
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
}
由于我们不再使用 uniform 来传递片段的颜色了,现在使用 ourColor 输出变量,我们必须再修改一下片段着色器:
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
void main()
{
FragColor = vec4(ourColor, 1.0);
}
因为我们添加了另一个顶点属性,并且更新了 VBO 的内存,我们就必须重新配置顶点属性指针。更新后的 VBO 内存中的数据现在看起来像这样:
知道了现在使用的布局,我们就可以使用 VertexAttribPointer 函数更新顶点格式
// 4. 设定顶点属性指针
gl.VertexAttribPointerWithOffset(0, 3, gl.FLOAT, false, 6 * 4, 0) // VertexAttribPointer 偏移已经被废弃
gl.EnableVertexAttribArray(0)
gl.VertexAttribPointerWithOffset(1, 3, gl.FLOAT, false, 6 * 4, 3*4)
gl.EnableVertexAttribArray(1)
由于我们现在有了两个顶点属性,我们不得不重新计算步长值。为获得数据队列中下一个属性值(比如位置向量的下个 x 分量)我们必须向右移动 6 个float,其中 3 个是位置值,另外 3 个是颜色值。这使我们的步长值为 6 乘以 float32 的字节数(=24字节)。
注意:VertexAttribPointer 偏移在 1.14 之后已经被废弃
// 1.14 before
gl.VertexAttribPointer(1, 3, gl.FLOAT, false, 6*4, gl.PtrOffset(3*4))
// after
gl.VertexAttribPointerWithOffset(1, 3, gl.FLOAT, false, 6 * 4, 3*4)
最后输出:
到目前为止的 代码
简单的封装 Shader 工具
到目前为止 Shader 读取,数据绑定等等都在一个文件里面,不利于后面的拓展学习,所以这里将其拆分开来,以便直接在硬盘里面读取 Shader 数据
首先创建一个 gfx/shader.go 文件
定义结构体
为它们定义一个 Shader 结构,及进程结构
package gfx
type Shader struct {
handle uint32
}
type Program struct {
handle uint32
shaders []*Shader
}
封装异常消息工具
再封装一个通用的取异常消息的函数
// 取得当前 handle 对象的入口函数
type getObjIv func(uint32, uint32, *int32)
// 取得 Log 的入口函数
type getObjInfoLog func(uint32, int32, *int32, *uint8)
// getGlError
// @Description: 取得错误消息
// @param glHandle 当前操作对象的 ID
// @param checkTrueParam 当前正在执行的操作
// @param getObjIvFn 取得当前 handle 对象的入口函数
// @param getObjInfoLogFn 取得 Log 的入口函数
// @param failMsg 错误消息
// @return error
//
func getGlError(
glHandle uint32,
checkTrueParam uint32,
getObjIvFn getObjIv,
getObjInfoLogFn getObjInfoLog,
failMsg string) error {
var success int32
getObjIvFn(glHandle, checkTrueParam, &success)
if success == gl.FALSE {
var logLength int32
getObjIvFn(glHandle, gl.INFO_LOG_LENGTH, &logLength)
log := gl.Str(strings.Repeat("\x00", int(logLength)))
getObjInfoLogFn(glHandle, logLength, nil, log)
return fmt.Errorf("%s: %s", failMsg, gl.GoStr(log))
}
return nil
}
使用例:
handle := gl.CreateShader(sType)
// ...
err := getGlError(handle, gl.COMPILE_STATUS, gl.GetShaderiv, gl.GetShaderInfoLog,
"SHADER::COMPILE_FAILURE::")
if err != nil {
panic(err)
}
定义 Shader 操作行为
主要是操作这个进程结构为主
func (shader *Shader) Delete() {
gl.DeleteShader(shader.handle)
}
func (prog *Program) Delete() {
for _, shader := range prog.shaders {
shader.Delete()
}
gl.DeleteProgram(prog.handle)
}
func (prog *Program) Attach(shaders ...*Shader) {
for _, shader := range shaders {
gl.AttachShader(prog.handle, shader.handle)
prog.shaders = append(prog.shaders, shader)
}
}
func (prog *Program) SetUniformF4(name string, v1, v2, v3, v4 float32) {
location := gl.GetUniformLocation(prog.handle, gl.Str(name+"\x00"))
gl.Uniform4f(location, v1, v2, v3, v4)
}
func (prog *Program) Use() {
gl.UseProgram(prog.handle)
}
func (prog *Program) Link() error {
gl.LinkProgram(prog.handle)
return getGlError(prog.handle, gl.LINK_STATUS, gl.GetProgramiv, gl.GetProgramInfoLog,
"PROGRAM::LINKING_FAILURE")
}
创建工厂函数
最后是添加一个工厂函数,里面逻辑也很简单,就是
func NewProgram(shaders ...*Shader) (*Program, error) {
prog := &Program{handle: gl.CreateProgram()}
prog.Attach(shaders...)
if err := prog.Link(); err != nil {
return nil, err
}
return prog, nil
}
func NewShader(src string, sType uint32) (*Shader, error) {
handle := gl.CreateShader(sType)
glSrc, freeFn := gl.Strs(src + "\x00")
defer freeFn()
gl.ShaderSource(handle, 1, glSrc, nil)
gl.CompileShader(handle)
err := getGlError(handle, gl.COMPILE_STATUS, gl.GetShaderiv, gl.GetShaderInfoLog,
"SHADER::COMPILE_FAILURE::")
if err != nil {
return nil, err
}
return &Shader{handle: handle}, nil
}
这里再补充一个从文件里面读取 Shader 的工具类
func NewShaderFromFile(file string, sType uint32) (*Shader, error) {
src, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
handle := gl.CreateShader(sType)
glSrc, freeFn := gl.Strs(string(src) + "\x00")
defer freeFn()
gl.ShaderSource(handle, 1, glSrc, nil)
gl.CompileShader(handle)
err = getGlError(handle, gl.COMPILE_STATUS, gl.GetShaderiv, gl.GetShaderInfoLog,
"SHADER::COMPILE_FAILURE::" + file)
if err != nil {
return nil, err
}
return &Shader{handle:handle}, nil
}
然后可以创建对应的着色器文件了
- 片段着色器
.frag
- 顶点着色器
.vert
简单使用
最后代码可以 这里 找到